/* 
 File: clutil.c 
 Functions to help with opencl management.
*/
#include "clutil.h"
#include<iostream>
#include<string.h>
#include<fstream>
#include<stdlib.h>
#include<time.h>
using namespace std;
// global variables
cl_context cxContext = 0;
cl_program cpProgram = 0;
cl_kernel kernel;
cl_command_queue commandQueue;
cl_event gpuExecution;
cl_event gpuDone;
cl_device_type device_type;
cl_device_id device;

// Default Configs
// host memory
double* hA = 0;
double* hB = 0;
double* hResult = 0;

// device memory
// d_Freal and d_Fimag represent the input signal to be transformed.
// d_Rreal and d_Rimag represent the transformed output.
cl_mem dA;
cl_mem dB;
cl_mem dResult;

unsigned
initExecution(const unsigned size)
{
#ifdef CPU
    device_type = CL_DEVICE_TYPE_CPU;
    cout << "CPU Execution" << endl;
#else
    device_type = CL_DEVICE_TYPE_GPU;
    cout << "GPU Execution" << endl;
#endif
    // Allocate host memory
    allocateHostMemory(size);
    cout << "Initializing device(s).." << endl;
    // create the OpenCL context on available GPU devices
    init_cl_context();

    const cl_uint ciDeviceCount =  getDeviceCount();

    if (!ciDeviceCount) {
        printf("No opencl specific devices!\n");
		return 0;
	}

	printf("Creating Command Queue...\n");
	createCommandQueue();
	return 1;
}

void
printGpuTime()
{
    cout << "Kernel execution Time on " 
         << ((device_type == CL_DEVICE_TYPE_GPU ) ? "GPU" : "CPU")
         << " : " << executionTime() << " ms" << endl;
}


void
verifyResult(const unsigned size)
{
    for (unsigned i = 0; i < size; ++i) {
        double result = hA[i] + (double)1.0;
      
        if (hResult[i] != result) {
           cout << "ERROR: " << i << " " <<  "Result " << hResult[i] 
                << " " <<  "Actual " << result << endl;
           cleanup();
           exit(0);
        }     
    } 
    cout << "Result Verified " << endl;
}

double 
executionTime()
{
    cl_ulong start, end;
    clGetEventProfilingInfo(gpuExecution, 
                            CL_PROFILING_COMMAND_END, 
                            sizeof(cl_ulong), &end, NULL);
    clGetEventProfilingInfo(gpuExecution,
                            CL_PROFILING_COMMAND_START, 
                            sizeof(cl_ulong), &start, NULL);

    const double time =  (double)1.0e-9 * (end - start) * 1000;
    const int flops = 128 * 2 * 16 * 15 *
                      128 * 256;
    cout << "Gflops: " <<  (flops / (time / 1000.0f)) / 1e9 << endl;

    return time;  
}

void
allocateHostMemory(const unsigned size)
{
    hA = (double*) malloc(sizeof(double) * size);

    hB = (double*) malloc(sizeof(double) * size);

    hResult = (double*) malloc(sizeof(double) * size);

    if (hA == 0 || hB == 0 || hResult == 0) {
        cout <<  "allocateHostMemory:: Could not allocate memory" << endl;
        cleanup();
        exit(EXIT_FAILURE);
    }
    srand((unsigned)time(NULL));
    
    for (unsigned i = 0 ; i < size; ++i) {
        //hA[i] = ((double)rand()/(double)256);
        hA[i] = 0;
		hResult[i] = 0;
    }
}

void
allocateDeviceMemory(const unsigned size)
                                      
{
    dA = createDeviceBuffer(CL_MEM_READ_ONLY,
                            sizeof(double) * size,
                             hA); 
    copyToDevice(dA, hA, size);

    dB = createDeviceBuffer(CL_MEM_READ_ONLY,
                            sizeof(double) * size,
                            hB);
    copyToDevice(dB, hB, size);


    dResult = createDeviceBuffer(CL_MEM_WRITE_ONLY,
                                 sizeof(double) * size,
                                 hResult);
    copyToDevice(dResult, hResult, size);
}

void 
cleanup()
{
	// clean up device
	if (dA) { 
		clReleaseMemObject(dA);
	}
    if (dB) {
        clReleaseMemObject(dB);
    }

	if (dResult) { 
		clReleaseMemObject(dResult);
	}
	// cleanup ocl routines
	if (gpuExecution) {  
		clReleaseEvent(gpuExecution);
	}
	if (gpuDone) {
		clReleaseEvent(gpuDone);
	}
	if (kernel) { 
		clReleaseKernel(kernel);
	}
	if (commandQueue) {
		clReleaseCommandQueue(commandQueue);
	}

	if (cpProgram) {
		clReleaseProgram(cpProgram);
	}
	if (cxContext) { 
		// segfaults if context is null.
		clReleaseContext(cxContext);
	}

	// Release mem and event objects 
	free(hA);
	free(hB);
	hA = 0;
	hB = 0;
	free(hResult);
	hResult = 0;
}

void
checkError(const cl_int ciErrNum,
           const char* const operation) 
{
    if (ciErrNum != CL_SUCCESS) {
        cout << "ERROR:: " << operation << " failed "
             << oclErrorString(ciErrNum) << endl; 
        cleanup();
        // TODO::        may be print the type of error
        exit(EXIT_FAILURE);
    }    
}

void
init_cl_context()
{
    cl_int ciErrNum = CL_SUCCESS; 
    cl_platform_id cpPlatform;
    
#if 0
    cxContext = clCreateContextFromType(0, /* cl_context_properties */
                                        device_type,
                                        NULL, /* error function ptr */
                                        NULL, /* user data to be passed to err fn */
                                        &ciErrNum)
#endif
    ciErrNum = clGetPlatformIDs(1, &cpPlatform, NULL);
    checkError(ciErrNum, "clGetPlatformIDs");

    ciErrNum = clGetDeviceIDs(cpPlatform, device_type, 1, 
                              &device, NULL);
    
    checkError(ciErrNum,  "clGetDeviceIDs");
    cxContext = clCreateContext(0, 1, &device, NULL, NULL, &ciErrNum); 

    checkError(ciErrNum, "clCreateContext");
}

cl_uint 
getDeviceCount()
{
    size_t nDeviceBytes;
  
    const cl_int ciErrNum = clGetContextInfo(cxContext,
                              CL_CONTEXT_DEVICES, /* Param Name */
                              0, /* size_t param_value_size  */
                              NULL, /* void * param_value */
                              &nDeviceBytes); /* size_t param_value_size_ret */
  
    checkError(ciErrNum, "clGetContextInfo");
    return ((cl_uint)nDeviceBytes/sizeof(cl_device_id));
}
      

void
createCommandQueue()
{
    cl_int ciErrNum = CL_SUCCESS;
    commandQueue  = clCreateCommandQueue(cxContext, 
                                         device,
                                         CL_QUEUE_PROFILING_ENABLE,
                                         &ciErrNum);
    checkError(ciErrNum, "clCreateCommandQueue"); 
}                                     

void
compileProgram(const char* const header_file, 
               const char* const kernel_file)
{

    // Load the OpenCL source code from the .cl file 
    /*const char* header_path = shrFindFilePath(header_file, argv[0]);
  
    size_t program_length;
    char* header = oclLoadProgSource(header_path, "" , &program_length);

    checkError((header != NULL), shrTRUE, "oclLoadProgSource on header");*/

    size_t program_length;
    char* const source = readFile(kernel_file, program_length);

    cl_int ciErrNum; 
    // Create the program for all GPUs in the context
    cpProgram = clCreateProgramWithSource( cxContext, 1,
                                           (const char **) &source,
                                           &program_length,
                                           &ciErrNum);
    //free(header);
    free(source);
     
    checkError(ciErrNum,  "clCreateProgramWithSource"); 
    /* Build program */

    ciErrNum = clBuildProgram(cpProgram,
                              0,        /* Number of devices for which we need to do this */
                              NULL, /* Device List */
                              "",
                              NULL, /* ptr to function */
                              NULL); /* User data to pass to ptrfn */

    if (ciErrNum != CL_SUCCESS) {
        buildError(); 
        checkError(ciErrNum, "clBuildProgram");
    }
}

char*
readFile(const char *fileName, size_t& length)
{
    ifstream is;
    is.open(fileName, ios_base::in);
    if (!is.is_open()) {
        cout << "File " << fileName << " cannot be opened" << endl; 
        cleanup();
        exit(EXIT_FAILURE); 
    }
    is.seekg (0, ios::end);
    length = is.tellg();
    is.seekg (0, ios::beg);
    char* buffer = (char*)malloc(length);
    is.read(buffer, length);
    is.close();
    return buffer;
}   


void
buildError()
{
	char *buildLog = NULL;
	size_t buildLogSize = 0;
	cl_int ciErrNum = clGetProgramBuildInfo (cpProgram,
			device,
			CL_PROGRAM_BUILD_LOG,
			buildLogSize,
			buildLog,
			&buildLogSize);
	checkError(ciErrNum, "clBuildProgramInfo");
	buildLog = (char*)malloc(buildLogSize);
	if(buildLog == NULL)
	{
		cout << "ERROR:: Not enough memory" << endl;
		cleanup();
		exit(EXIT_FAILURE);
	}
	memset(buildLog, 0, buildLogSize);
	ciErrNum = clGetProgramBuildInfo (cpProgram,
			device,
			CL_PROGRAM_BUILD_LOG,
			buildLogSize,
			buildLog,
			NULL);
	checkError(ciErrNum, "clBuildProgramInfo");
	std::cout << " \n\t\t\tBUILD LOG\n";
	std::cout << " ************************************************\n";
	std::cout << buildLog << std::endl;
	std::cout << " ************************************************\n";
	free(buildLog);
}

void
createKernel(const char* const kernelName)
{
    cl_int ciErrNum = CL_SUCCESS;
    kernel = clCreateKernel(cpProgram, kernelName, &ciErrNum);
    checkError(ciErrNum, "clCreateKernel");
}


cl_mem 
createDeviceBuffer(const cl_mem_flags flags, 
                   const size_t size, void* const hostPtr)
                   


{
    cl_int ciErrNum = CL_SUCCESS;
    const cl_mem d_mem = clCreateBuffer(cxContext,
                                        flags,
                                        size,
                                        0,
                                        &ciErrNum);
  
    checkError(ciErrNum, "clCreateBuffer");
    return d_mem;
}

void 
copyToDevice(const cl_mem mem, void* const hostPtr, 
             const unsigned size)
{
    const cl_int ciErrNum = clEnqueueWriteBuffer(commandQueue, 
                                                mem, CL_TRUE, 0, 
                                                sizeof(double) * size, hostPtr, 
                                                0, NULL, NULL);
    checkError(ciErrNum, "clEnqueueWriteBuffer");
}

void
copyFromDevice(const cl_mem dMem, void* const hostPtr, 
               const unsigned size)
{
    cl_int ciErrNum = clEnqueueReadBuffer(commandQueue, dMem,
                                          CL_FALSE, 0,
                                          sizeof(double) * size,
                                          hostPtr, 0, NULL, 
                                          &gpuDone);
    checkError(ciErrNum, "clEnqueueReadBuffer"); 
}


void 
runKernel(size_t localWorkSize, 
          size_t globalWorkSize)
{
	cl_int ciErrNum;
	size_t maxWorkGroupSize;
	ciErrNum = clGetDeviceInfo(
			device,
			CL_DEVICE_MAX_WORK_GROUP_SIZE,
			sizeof(size_t),
			(void *)&maxWorkGroupSize,
			NULL);
    checkError(ciErrNum, "clGetDeviceInfo");
    cout << "Global Work Size " << globalWorkSize << endl
         << "Local Work Size " << localWorkSize << endl
         << "# of Work Groups " << (globalWorkSize % localWorkSize 
                                   + globalWorkSize / localWorkSize) << endl;

    cout << "Max size: " << maxWorkGroupSize << endl;

	ciErrNum = clEnqueueNDRangeKernel(commandQueue, 
			kernel, 
			1, NULL, 
			&globalWorkSize, 
			&localWorkSize, 0, 
			NULL, 
			&gpuExecution);
	checkError(ciErrNum, "clEnqueueNDRangeKernel");
}

const char* oclErrorString(cl_int error)
{
    static const char* errorstring[] = {
        "cl_success",
        "cl_device_not_found",
        "cl_device_not_available",
        "cl_compiler_not_available",
        "cl_mem_object_allocation_failure",
        "cl_out_of_resources",
        "cl_out_of_host_memory",
        "cl_profiling_info_not_available",
        "cl_mem_copy_overlap",
        "cl_image_format_mismatch",
        "cl_image_format_not_supported",
        "cl_build_program_failure",
        "cl_map_failure",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "cl_invalid_value",
        "cl_invalid_device_type",
        "cl_invalid_platform",
        "cl_invalid_device",
        "cl_invalid_context",
        "cl_invalid_queue_properties",
        "cl_invalid_command_queue",
        "cl_invalid_host_ptr",
        "cl_invalid_mem_object",
        "cl_invalid_image_format_descriptor",
        "cl_invalid_image_size",
        "cl_invalid_sampler",
        "cl_invalid_binary",
        "cl_invalid_build_options",
        "cl_invalid_program",
        "cl_invalid_program_executable",
        "cl_invalid_kernel_name",
        "cl_invalid_kernel_definition",
        "cl_invalid_kernel",
        "cl_invalid_arg_index",
        "cl_invalid_arg_value",
        "cl_invalid_arg_size",
        "cl_invalid_kernel_args",
        "cl_invalid_work_dimension",
        "cl_invalid_work_group_size",
        "cl_invalid_work_item_size",
        "cl_invalid_global_offset",
        "cl_invalid_event_wait_list",
        "cl_invalid_event",
        "cl_invalid_operation",
        "cl_invalid_gl_object",
        "cl_invalid_buffer_size",
        "cl_invalid_mip_level",
        "cl_invalid_global_work_size",
    };

    const int errorcount = sizeof(errorstring) / sizeof(errorstring[0]);

    const int index = -error;

    return (index >= 0 && index < errorcount) ? errorstring[index] : "";
}
